package com.liu.item.controller;

import com.alibaba.fastjson.JSON;
import com.liu.item.feign.api.GoodsService;
import com.liu.shop.common.constant.RedisConstatns;
import com.liu.shop.common.exception.BusinessException;
import com.liu.shop.common.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.concurrent.TimeUnit;

/**
 * 商品详情页
 */
@Controller
@RequestMapping("/item")
@Slf4j
public class ItemController {

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 用户点击商品时跳转到商品详情页面
     * 先查询缓存，缓存查不到再查数据库
     *  1、使用分布式锁、锁自动释放
     * 2、拿到锁后处理义务出现了异常
     * 3、释放锁要使用luna脚本，需要保证原子性
     * 4、获取到锁后执行业务超时的情况(启动一个定时任务来给这个锁续期)
     * 5、缓存穿透
     * a)出现的场景:用户在查询一条数据，缓存中没有，数据库中也没有，必须商品id是-1，100
     *      b)如何解决
     *          1)如果数据库也没有，需要给缓存设置默认值
     * 6、缓存击穿
     *      a)出现的场景：缓存中没有，但是数据库中有
     *      b)如何解决，分布式锁来解决
     * @param id
     * @return
     */
    @RequestMapping("/{id}.html")
    public String itemPage(@PathVariable Integer id, ModelMap modelMap){

        // 调用查询商品详细信息方法
        String goods = getGoods(id);

        if ("null".equals(goods)) {
            log.debug("商品没有查询到，抛出异常了，【{}】",Thread.currentThread().getId());
            throw new BusinessException("该商品不存在", 102);
        }

        // 把JSON字符串转成对象
        GoodsVo goodsVo = JSON.parseObject(goods, GoodsVo.class);
        // 放到域中
        modelMap.addAttribute("goodsVo", goodsVo);

        // 返回视图页面
        return "item";

    }
    // 解决各种缓存问题后，将商品信息返回
//    private String getGoods(Integer id){
//        // 1.先查询缓存
//        String goods = redisTemplate.opsForValue().get(RedisConstatns.ITEM_PRE+id);
//
//        // 2.判断缓存是否为空
//        if(StringUtils.isEmpty(goods)){
//            // 生成uuid，让每把锁都不一样，防止第一个线程的过期时间将第二个线程的锁释放了
//            String uuid = UUID.randomUUID().toString();
//
//            try {
//                // 先获取锁(排队)，考虑到自动释放,设置1秒的redis过期时间
//                Boolean lock = redisTemplate.opsForValue().setIfAbsent(RedisConstatns.ITEM_LOCK, uuid, 1, TimeUnit.SECONDS);
//                if(lock){// 拿到锁的线程
//                    // 3.查询数据库，根据id调用商品模块查询商品信息
//                    GoodsVo goodsVo = goodsService.getGoodsVoById(id);
//                    // 解决缓存穿透的问题
//                    if (goodsVo != null) {
//                        // 把对象转成JSON字符串，然后在存在缓存中
//                        goods = JSON.toJSONString(goodsVo);
//                        // 4、添加到缓存中
//                        redisTemplate.opsForValue().set(RedisConstatns.ITEM_PRE + id, goods, 7, TimeUnit.DAYS);
//                    } else {// 为空就将null存入redis中
//                        goods = "null";
//                        redisTemplate.opsForValue().set(RedisConstatns.ITEM_PRE + id, goods, 7, TimeUnit.DAYS);
//                    }
//                    return goods;
//                }else {
//                    try {
//                        Thread.sleep(300);
//                        getGoods(id);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                }
//            }finally {//释放锁
////                String redisUiid = redisTemplate.opsForValue().get(ITEM_LOCK);
////                if (uuid.equals(redisUiid)) {
////                // 手动释放锁
////                    redisTemplate.delete(ITEM_LOCK);
////                }
//                String lunaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
//                        "then\n" +
//                        "    return redis.call(\"del\",KEYS[1])\n" +
//                        "else\n" +
//                        "    return 0\n" +
//                        "end";
//                // 使用luna脚本释放锁，因为luna脚本是原子性的
//                DefaultRedisScript defaultRedisScript = new DefaultRedisScript(lunaScript, Long.class);
//                redisTemplate.execute(defaultRedisScript, Arrays.asList(RedisConstatns.ITEM_LOCK), uuid);
//            }
//        }
//        return goods;
//    }
    @Autowired
    private RedissonClient redissonClient;
    // 使用redisson实现分布式锁
    private String getGoods(Integer id) {

        // 1.先查询缓存
        String goods = redisTemplate.opsForValue().get(RedisConstatns.ITEM_PRE + id);

        // 获取锁
        RLock itemLock = redissonClient.getLock(RedisConstatns.ITEM_LOCK); // hash结构 key[item-lock] feid[uuid] value[1]

        // 2.判断缓存是否为空
        if (StringUtils.isEmpty(goods)) {
            // 加锁，加锁失败等待1s就返回
            boolean lock = false;
            try {
                lock = itemLock.tryLock(1, 1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
//                e.printStackTrace();
                log.error("获取锁失败", e);
            }
            if (lock) {// 拿到钥匙的线程
                try {
                    // 3.查询数据库
                    GoodsVo goodsVo = goodsService.getGoodsVoById(id);
                    System.out.println("商品信息："+goodsVo);
                    // 解决缓存穿透的问题
                    // 用户直接查询数据库中不存在的数据，因为缓存中没有就直接查询数据库，会导致数据库崩溃。
                    // 因此对于这种数据直接存入一个null到redis中，就算发送再多请求查的也是null。
                    if (goodsVo != null) {// 该数据存在
                        // 把对象转成JSON字符串，然后在存在缓存中
                        goods = JSON.toJSONString(goodsVo);
                        // 4、添加到缓存中
                        redisTemplate.opsForValue().set(RedisConstatns.ITEM_PRE + id, goods, 7, TimeUnit.DAYS);
                        // 返回从数据库中查询的数据
                    } else {// 该数据不存在，直接存入null
                        goods = "null";
                        redisTemplate.opsForValue().set(RedisConstatns.ITEM_PRE + id, goods, 7, TimeUnit.DAYS);
                    }
                } finally {
                    // 释放锁
                    itemLock.unlock();
                }
            } else {// 没有拿到钥匙的线程
                try {
                    Thread.sleep(300);
                    getGoods(id);// 没有拿到钥匙的线程再次调用方法，进行查询
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return goods;
    }

}
